31d9b578fc68e253c51ab75ca67022f57e7fd992
[nextcloud-desktop.git] /
1 //
2 //  ShareTableViewDataSource.swift
3 //  FileProviderUIExt
4 //
5 //  Created by Claudio Cambra on 27/2/24.
6 //
7
8 import AppKit
9 import FileProvider
10 import NextcloudKit
11 import NextcloudFileProviderKit
12 import NextcloudCapabilitiesKit
13 import OSLog
14
15 class ShareTableViewDataSource: NSObject, NSTableViewDataSource, NSTableViewDelegate {
16     private let shareItemViewIdentifier = NSUserInterfaceItemIdentifier("ShareTableItemView")
17     private let shareItemViewNib = NSNib(nibNamed: "ShareTableItemView", bundle: nil)
18     private let reattemptInterval: TimeInterval = 3.0
19
20     var uiDelegate: ShareViewDataSourceUIDelegate?
21     var sharesTableView: NSTableView? {
22         didSet {
23             sharesTableView?.register(shareItemViewNib, forIdentifier: shareItemViewIdentifier)
24             sharesTableView?.rowHeight = 42.0  // Height of view in ShareTableItemView XIB
25             sharesTableView?.dataSource = self
26             sharesTableView?.delegate = self
27             sharesTableView?.reloadData()
28         }
29     }
30     var capabilities: Capabilities?
31     var itemMetadata: NKFile?
32
33     private(set) var kit: NextcloudKit?
34     private(set) var itemURL: URL?
35     private(set) var itemServerRelativePath: String?
36     private(set) var shares: [NKShare] = [] {
37         didSet { Task { @MainActor in sharesTableView?.reloadData() } }
38     }
39     private var account: Account? {
40         didSet {
41             guard let account = account else { return }
42             kit = NextcloudKit()
43             kit?.setup(
44                 user: account.username,
45                 userId: account.username,
46                 password: account.password,
47                 urlBase: account.serverUrl
48             )
49         }
50     }
51
52     func loadItem(url: URL) {
53         itemServerRelativePath = nil
54         itemURL = url
55         Task {
56             await reload()
57         }
58     }
59
60     func reattempt() {
61         DispatchQueue.main.async {
62             Timer.scheduledTimer(withTimeInterval: self.reattemptInterval, repeats: false) { _ in
63                 Task { await self.reload() }
64             }
65         }
66     }
67
68     func reload() async {
69         guard let itemURL else {
70             presentError("No item URL, cannot reload data!")
71             return
72         }
73         guard let itemIdentifier = await withCheckedContinuation({
74             (continuation: CheckedContinuation<NSFileProviderItemIdentifier?, Never>) -> Void in
75             NSFileProviderManager.getIdentifierForUserVisibleFile(
76                 at: itemURL
77             ) { identifier, domainIdentifier, error in
78                 defer { continuation.resume(returning: identifier) }
79                 guard error == nil else {
80                     self.presentError("No item with identifier: \(error.debugDescription)")
81                     return
82                 }
83             }
84         }) else {
85             presentError("Could not get identifier for item, no shares can be acquired.")
86             return
87         }
88
89         do {
90             let connection = try await serviceConnection(url: itemURL, interruptionHandler: {
91                 Logger.sharesDataSource.error("Service connection interrupted")
92             })
93             guard let serverPath = await connection.itemServerPath(identifier: itemIdentifier),
94                   let credentials = await connection.credentials() as? Dictionary<String, String>,
95                   let convertedAccount = Account(dictionary: credentials),
96                   !convertedAccount.password.isEmpty
97             else {
98                 presentError("Failed to get details from File Provider Extension. Retrying.")
99                 reattempt()
100                 return
101             }
102             let serverPathString = serverPath as String
103             itemServerRelativePath = serverPathString
104             account = convertedAccount
105             await sharesTableView?.deselectAll(self)
106             capabilities = await fetchCapabilities()
107             guard capabilities != nil else { return }
108             guard capabilities?.filesSharing?.apiEnabled == true else {
109                 presentError("Server does not support shares.")
110                 return
111             }
112             guard let kit else {
113                 presentError("NextcloudKit instance is unavailable, cannot reload data!")
114                 return
115             }
116             itemMetadata = await fetchItemMetadata(itemRelativePath: serverPathString, kit: kit)
117             guard itemMetadata?.permissions.contains("R") == true else {
118                 presentError("This file cannot be shared.")
119                 return
120             }
121             shares = await fetch(
122                 itemIdentifier: itemIdentifier, itemRelativePath: serverPathString
123             )
124         } catch let error {
125             presentError("Could not reload data: \(error), will try again.")
126             reattempt()
127         }
128     }
129
130     private func fetch(
131         itemIdentifier: NSFileProviderItemIdentifier, itemRelativePath: String
132     ) async -> [NKShare] {
133         Task { @MainActor in uiDelegate?.fetchStarted() }
134         defer { Task { @MainActor in uiDelegate?.fetchFinished() } }
135
136         let rawIdentifier = itemIdentifier.rawValue
137         Logger.sharesDataSource.info("Fetching shares for item \(rawIdentifier, privacy: .public)")
138
139         guard let kit = kit else {
140             self.presentError("NextcloudKit instance is unavailable, cannot fetch shares!")
141             return []
142         }
143
144         let parameter = NKShareParameter(path: itemRelativePath)
145
146         return await withCheckedContinuation { continuation in
147             kit.readShares(parameters: parameter) { account, shares, data, error in
148                 let shareCount = shares?.count ?? 0
149                 Logger.sharesDataSource.info("Received \(shareCount, privacy: .public) shares")
150                 defer { continuation.resume(returning: shares ?? []) }
151                 guard error == .success else {
152                     self.presentError("Error fetching shares: \(error.errorDescription)")
153                     return
154                 }
155             }
156         }
157     }
158
159     private func fetchCapabilities() async -> Capabilities? {
160         return await withCheckedContinuation { continuation in
161             kit?.getCapabilities { account, capabilitiesJson, error in
162                 guard error == .success, let capabilitiesJson = capabilitiesJson else {
163                     self.presentError("Error getting server caps: \(error.errorDescription)")
164                     continuation.resume(returning: nil)
165                     return
166                 }
167                 Logger.sharesDataSource.info("Successfully retrieved server share capabilities")
168                 continuation.resume(returning: Capabilities(data: capabilitiesJson))
169             }
170         }
171     }
172
173     private func presentError(_ errorString: String) {
174         Logger.sharesDataSource.error("\(errorString, privacy: .public)")
175         Task { @MainActor in self.uiDelegate?.showError(errorString) }
176     }
177
178     // MARK: - NSTableViewDataSource protocol methods
179
180     @objc func numberOfRows(in tableView: NSTableView) -> Int {
181         shares.count
182     }
183
184     // MARK: - NSTableViewDelegate protocol methods
185
186     @objc func tableView(
187         _ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int
188     ) -> NSView? {
189         let share = shares[row]
190         guard let view = tableView.makeView(
191             withIdentifier: shareItemViewIdentifier, owner: self
192         ) as? ShareTableItemView else {
193             Logger.sharesDataSource.error("Acquired item view from table is not a share item view!")
194             return nil
195         }
196         view.share = share
197         return view
198     }
199
200     @objc func tableViewSelectionDidChange(_ notification: Notification) {
201         guard let selectedRow = sharesTableView?.selectedRow, selectedRow >= 0 else {
202             Task { @MainActor in uiDelegate?.hideOptions(self) }
203             return
204         }
205         let share = shares[selectedRow]
206         Task { @MainActor in uiDelegate?.showOptions(share: share) }
207     }
208 }